﻿using System;
using System.Dynamic;
using System.Text;
using System.Collections.Generic;
using Xunit;
using Xunit.Sdk;
using Toolkit.Csv;

namespace Toolkit.Csv.Test
{
    public class ParserTests
    {
        private void TestParseString(string input, char sep, string[][] expected)
        {
            // Parse the file as 2d array and compare
            var actual = CsvParser.ParseText(sep, input);
            Assert.True(expected.SameArrays(actual),
                        string.Format("Expected Array:\n{0}\nActual Array:\n{1}", expected.ListToCsv(),
                                        actual.ListToCsv()));

            if (expected.IsRectangular())
            {
                // Parse the text as dictionaries and compare
                var actualDicts = CsvParser.ParseTextAsDictionaries(sep, input);
                var expectedDicts = expected.ListToDicts();
                Assert.True(expectedDicts.SameDicts(actualDicts),
                            string.Format("Expected Dictionaries:\n{0}\nActual Dictionaries:\n{1}", expectedDicts.DictsToString(),
                                          actualDicts.DictsToString()));
            }
        }

        private void TestParseStringFailure(string input, char sep, string[][] expected)
        {
            // Parse the file as 2d array and compare
            var actual = CsvParser.ParseText(sep, input);
            Assert.True(expected.SameArrays(actual),
                        string.Format("Expected Array:\n{0}\nActual Array:\n{1}", expected.ListToCsv(),
                                        actual.ListToCsv()));

            MustThrowException(CsvParser.ParseTextAsDynamics, sep, input, "ParseTextAsDynamics");
        }

        private void TestParseFile(string filename, char sep, string[][] expected)
        {
            // Parse the file as 2d array and compare
            var actual = CsvParser.ParseFile(sep, filename);
            Assert.True(expected.SameArrays(actual), string.Format("Expected Arrays:\n{0}\nActual Arrays:\n{1}", expected.ListToCsv(), actual.ListToCsv()));

            if (expected.IsRectangular())
            {
                // Parse the file as dictionaries and compare
                var actualDicts = CsvParser.ParseFileAsDictionaries(sep, filename);
                var expectedDicts = expected.ListToDicts();
                Assert.True(expectedDicts.SameDicts(actualDicts),
                            string.Format("Expected Dictionaries:\n{0}\nActual Dictionaries:\n{1}", expectedDicts.DictsToString(),
                                          actualDicts.DictsToString()));
            }
        }

        private void TestParseFileFailure(string filename, char sep)
        {
            MustThrowException(CsvParser.ParseFile, sep, filename, "ParseFile");
            MustThrowException(CsvParser.ParseFileAsDictionaries, sep, filename, "ParseFileAsDictionaries");
            MustThrowException(CsvParser.ParseFileAsDynamics, sep, filename, "ParseFileAsDynamics");
        }

        private void MustThrowException<T>(Func<char,string,T> func, char sep, string input, string methodName)
        {
            try
            {
                func(sep, input);
                Assert.True(false, "Exception should have been thrown by " + methodName);
            }
            catch(Exception)
            {
                // Ok
            }
        }

        [Fact]
        public void ParseDynamics()
        {
            const string input = "first,last\nJohn,Smith\nJane,Doe";
            var actual = CsvParser.ParseTextAsDynamics(',', input);
            var john = actual[0];

            Assert.True(john.first == "John" && john.last == "Smith",
                        "John Smith was not parsed correctly: got: " + HelperExtensions.DynToString(john));
            dynamic jane = actual[1];
            Assert.True(jane.first == "Jane" && jane.last == "Doe",
                        "Jane Doe was not parsed correctly: got: " + HelperExtensions.DynToString(jane));
        }

        // Test class for parsing as custom type
        class Person
        {
            public string name { get; set; }
            public DateTime dob { get; set; }
            public int age { get; set; }
            public long? FavoriteNumber { get; set; }

            public override string ToString()
            {
                return string.Format("Name: {0}, Age: {1}, FavoriteNumber: {2}, DOB: {3}", name, age, FavoriteNumber, dob);
            }

            public override bool Equals(object obj)
            {
                var other = obj as Person;
                return other != null && other.name == name && other.age == age && other.dob.Equals(dob) &&
                       (FavoriteNumber == null ? other.FavoriteNumber == null : other.FavoriteNumber == FavoriteNumber);
            }
        }

        [Fact]
        public void ParseTypesWithHeaders()
        {
            var actual = CsvParser.ParseFileAs<Person>(',', "../../data/types1.csv");
            var john = actual[0];
            var will = actual[1];

            var realJohn = new Person {name = "John", age = 53, dob = new DateTime(1959, 5, 12), FavoriteNumber = 42};
            var realWill = new Person { name = "Will", age = 43, dob = new DateTime(1968, 9, 25), FavoriteNumber = null };

            Assert.True(john.Equals(realJohn),
                        "John was not parsed correctly: got: " + john);
            Assert.True(will.Equals(realWill),
                        "Will was not parsed correctly: got: " + will);
        }

        [Fact]
        public void ParseTypesWithoutHeaders()
        {
            var actual = CsvParser.ParseFileAs<Person>(',', "../../data/types2.csv");
            var john = actual[0];
            var will = actual[1];

            var realJohn = new Person { name = "John", age = 53, dob = new DateTime(1959, 5, 12), FavoriteNumber = 42 };
            var realWill = new Person { name = "Will", age = 43, dob = new DateTime(1968, 9, 25), FavoriteNumber = null };

            Assert.True(john.Equals(realJohn),
                        "John was not parsed correctly: got: " + john);
            Assert.True(will.Equals(realWill),
                        "Will was not parsed correctly: got: " + will);
        }

        [Fact]
        public void ParseSimple()
        {   //                    hello,world
            //                    foo,bar
            const string input = "hello,world\nfoo,bar";
            var expected = new [] {
                new []{"hello", "world"},
                new []{"foo",   "bar"}
            };
            TestParseString(input, ',', expected);
        }

        [Fact]
        public void ParseEmpty()
        {   //                    hello,|
            //                    ,bar|
            //                    ,|
            //                    |
            //                    ,"","  ",|
            //                    , , |
            const string input = "hello,\n,bar\n,\n,\"\",\"  \",\n, , ";
            var expected = new[] {
                new []{"hello", ""},
                new []{"", "bar"},
                new []{"", ""},
                new []{"", "", "  ", ""},
                new []{"", "", ""}
            };
            TestParseString(input, ',', expected);
        }

        [Fact]
        public void ParseNothing()
        {// Completely empty text
            const string input = "";
            var expected = new string[][]{};
            TestParseStringFailure(input, ',', expected);
            TestParseFileFailure("../../data/empty.csv", ',');
        }

        [Fact]
        public void ParseQuotes()
        {   //                     "hello",world
            //                     foo,"bar"
            const string input = "\"hello\",world\nfoo,\"bar\"";
            var expected = new [] {
                new []{"hello",  "world"},
                new []{"foo",    "bar"}
            };
            TestParseString(input, ',', expected);
        }

        [Fact]
        public void ParseQuotesInQuotes()
        {   //                       "h""ello","""world"
            //                       "fo""""o","bar"""
            const string input = "\"h\"\"ello\",\"\"\"world\"\n\"fo\"\"\"\"o\",\"bar\"\"\"";
            var expected = new [] {
                new []{"h\"ello",   "\"world"},
                new []{"fo\"\"o",   "bar\""}
            };
            TestParseString(input, ',', expected);
        }

        [Fact]
        public void ParseWhitespaces()
        {   //                   |  hello    , "wo rld"   |
            //                   |    "     foo ",bar|
            const string input = "  hel lo    , \"wo rld\"   \n    \"     foo \",bar";
            var expected = new[] {
                new []{"hel lo",     "wo rld"},
                new []{"     foo ", "bar"}
            };
            TestParseString(input, ',', expected);
        }

        [Fact]
        public void ParseNewlines()
        {   //                   hello\n
            //                   world\r\n
            //                   foo\rbar\n
            //                   "multi\nline\r\ntext"\n
            const string input = "hello\nworld\r\nfoo\rbar\n\"multi\nline\r\ntext\"\n";
            var expected = new[] {
                new []{"hello"},
                new []{"world"},
                new []{"foo\rbar"},
                new []{"multi\nline\r\ntext"}
            };
            TestParseString(input, ',', expected);
        }

        [Fact]
        public void ParseJagged()
        {   //                     hello
            //                     world, foo,"bar"
            //                     two, more
            const string input = "hello\nworld, foo,\"bar\"\r\ntwo, more";
            var expected = new[] {
                new []{"hello"},
                new []{"world", "foo", "bar"}, 
                new []{"two", "more"}
            };
            TestParseString(input, ',', expected);
        }

        [Fact]
        public void TestDelimiters()
        {   //                     , hello
            //                     "in, quotes",,
            foreach (var sep in new []{',', '|', '\t', ';'})
            {
                var input = string.Format("{0} hello\n\"in{0} quotes\"{0}{0}", sep);
                var expected = new[]
                    {
                        new[] {"", "hello"},
                        new[] {string.Format("in{0} quotes", sep), "", ""}
                    };
                TestParseString(input, sep, expected);
            }
        }

        [Fact]
        public void CommaFile1()
        {
            var expected = new[]
                {
                    new[] {"hello","world","1","2","3"},
                    new[] {"3","2","1","world","hello"}
                };
            TestParseFile("../../data/comma1.csv", ',', expected);
        }

        [Fact]
        public void CommaFile2()
        {
            var expected = new[]
                {
                    new[] {"header1", "header2", "header3"},
                    new[] {"this example", "has", "some strange"},
                    new[] {"whitespaces\r\nand\r\nnewlines", "doesn't it?", "it does"},
                    new[] {"You can \"quote\" me on that", "", "\"quote\""},
                    new[] {"Commas, like these, can be used inside quotes, even when they are the delimiter.", ",,,", " , "},
                    new[] {"They do not work", "however", "if the text is not quoted"}
                };
            TestParseFile("../../data/comma2.csv", ',', expected);
        }

        [Fact]
        public void PipeFile1()
        {
            var expected = new[]
                {
                    new[] {"header1", "header2", "header3"},
                    new[] {"this example", "has", "some strange"},
                    new[] {"whitespaces\r\nand\r\nnewlines", "doesn't it?", "it does"},
                    new[] {"You can \"quote\" me on that", "", "\"quote\""},
                    new[] {"Pipes| like these| can be used inside quotes| even when they are the delimiter.", "|,|", " | "},
                    new[] {"They do not work", "however", "if the text is not quoted"}
                };
            TestParseFile("../../data/pipe1.csv", '|', expected);
        }

        [Fact]
        public void TabFile1()
        {
            var expected = new[]
                {
                    new[] {"headers", "are", "no", "good", "in", "this", "file"},
                    new[] {"this example", "has", "some strange"},
                    new[] {"whitespaces\r\nand\r\nnewlines", "doesn't it?", "it does"},
                    new[] {"You can \"quote\" me on that", "", "\"quote\""},
                    new[] {"Pipes\t like these\t can be used inside quotes\t even when they are the delimiter.", "\t,\t", " \t "},
                    new[] {"They do not work", "however", "if the text is not quoted"}
                };
            TestParseFile("../../data/tab1.csv", '\t', expected);
        }

        /*
         * This test is to judge performance only
         * Don't want to commit a huge CSV file to SVN
         * 
        [Fact]
        public void LargeDataset()
        {
            var parser = new CsvParser { Separator = ',' };
            CsvParser.ParseFileAsDynamics("../../data/large.csv");
        }
        */
    }
}
